package uu.tools.edge;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Session {
    private Map<String, Map<String, Item>> map = new HashMap<>();
    private ReentrantReadWriteLock readWriteLock;
    private Lock readLock;
    private Lock writeLock;
    private long expiration = 0L;

    public void put(String user, String key, Object value) {
        put(user, key, value, expiration);
    }

    public void put(String user, String key, Object value, Duration duration) {
        put(user, key, value, duration.toMillis());
    }

    public void put(String user, String key, Object value, long duration) {
        if (value == null || duration < 0) {
            return;
        }
        writeLock.lock();
        try {
            Map<String, Item> map = this.map.get(user);
            if (map == null) {
                this.map.put(user, map = new HashMap<>());
            }
            map.put(key, wrap(value, duration));
        } finally {
            writeLock.unlock();
        }
    }

    public void clear(String user) {
        writeLock.lock();
        try {
            Map<String, Item> map = this.map.get(user);
            if (map != null) {
                map.clear();
            }
        } finally {
            writeLock.unlock();
        }
    }

    public <T> T get(String user, String key) {
        readLock.lock();
        try {
            Map<String, Item> map = this.map.get(user);
            if (map == null) {
                return null;
            }
            return (T)unwrap(map.get(key));
        } catch (Exception e) {
            return null;
        } finally {
            readLock.unlock();
        }
    }

    public <T> T remove(String user, String key) {
        writeLock.lock();
        try {
            Map<String, Item> map = this.map.get(user);
            if (map == null) {
                return null;
            }
            return (T)unwrap(map.remove(key));
        } catch (Exception e) {
            return null;
        } finally {
            writeLock.unlock();
        }
    }

    public boolean has(String user, String key) {
        readLock.lock();
        try {
            Map<String, Item> map = this.map.get(user);
            if (map == null) {
                return false;
            }
            return unwrap(map.get(key)) != null;
        } finally {
            readLock.unlock();
        }
    }

    /**
     * 清理过期的 key, 由外部调用
     */
    public void clearAll() {
        writeLock.lock();
        try {
            this.map.entrySet().removeIf(entry -> {
                entry.getValue().entrySet().removeIf(item -> unwrap(item.getValue()) == null);
                return entry.getValue().isEmpty();
            });
        } finally {
            writeLock.unlock();
        }
    }

    private Object unwrap(Item item) {
        return item == null || item.isExpired() ? null : item.getValue();
    }

    private Item wrap(Object value, long duration) {
        return new Item(value, duration == 0L ? 0L : duration + System.currentTimeMillis());
    }

    private Session(Builder builder) {
        this.expiration = builder.expiration;
        this.readWriteLock = builder.lock == null ? new ReentrantReadWriteLock() : builder.lock;
        readLock = readWriteLock.readLock();
        writeLock = readWriteLock.writeLock();
    }

    public static Builder builder() {
        return new Builder();
    }

    @Data
    @AllArgsConstructor
    static final class Item {
        private Object value;
        private long expiration;

        /* 是否过期 */
        private boolean isExpired() {
            return expiration != 0L && expiration < System.currentTimeMillis();
        }
    }

    public static final class Builder {
        private long expiration;
        private ReentrantReadWriteLock lock;

        public Session build() {
            return new Session(this);
        }

        public Builder expiration(Duration duration) {
            this.expiration = duration.toMillis();
            return this;
        }

        public Builder lock(ReentrantReadWriteLock lock) {
            this.lock = lock;
            return this;
        }

        private Builder() {
        }
    }

}
